在 kintone 的客製化開發中,常會透過 JavaScript API 註冊各種事件處理器(event handler),讓程式碼在特定情境下被觸發執行。kintone 提供了多種事件可供使用,這次我們要介紹主題是:在新增或編輯記錄畫面中,欄位值變更時所觸發的 app.record.create.change.<欄位代碼>
與 app.record.edit.change.<欄位代碼>
事件。
kintone 提供以下五種類型的 change 事件:
app.record.create.change.<欄位代碼>
mobile.app.record.create.change.<欄位代碼>
app.record.edit.change.<欄位代碼>
mobile.app.record.edit.change.<欄位代碼>
app.record.index.edit.change.<欄位代碼>
這類事件必須指定欄位代碼,當該欄位的值在對應畫面中發生變更時,便會觸發所綁定的事件處理器。支援 change 事件的欄位類型包括:
需要特別注意的是,「Lookup」和「計算」欄位本身無法觸發 change 事件。但仍可透過監聽 Lookup 所帶入的其他欄位 來判斷是否有發生變化。例如,可以設定一個數值欄位接收 Lookup 帶入的來源記錄編號,然後監聽 app.record.edit.change.<來源記錄編號的欄位代碼>
,以達成類似的偵測效果。
針對表格內部的變動,有以下兩種類型:
app.record.edit.change.<表格內欄位代碼>
app.record.edit.change.<表格本身欄位代碼>
在 kintone 的客製化中,我們經常會遇到需要透過 API 取得資料的非同步處理情境。舉個簡單的例子:
假設在儲存記錄時需要自動產生一組流水號,可以這樣實作:
kintone.events.on('app.record.create.submit', async event => {
const { record } = event
// 透過 API 取得最新一筆記錄
const res = await kintone.api(kintone.api.url('/k/v1/records', true), 'GET', {
app: kintone.app.getId(),
query: 'order by $id desc limit 1',
})
const lastNum = res.records.length > 0 ? Number(res.records[0].流水號.value) : 0
record.流水號.value = String(lastNum + 1).padStart(8, '0')
return event
})
這段程式碼中,我們在 app.record.create.submit
事件中使用了 async
函式搭配 await
來處理非同步資料取得,是被支援的寫法。
然而,如果在欄位變更時也想要取得資料,例如使用者在 Lookup 欄位帶入資料後,自動從來源應用程式取得表格資料並帶入當前記錄,就會遇到一個常見問題。
你可能會這樣寫:
kintone.events.on('app.record.edit.change.來源記錄號碼', async event => {
const { record } = event
const recordId = record.來源記錄號碼.value
const res = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {
app: 來源應用程式ID,
id: recordId,
})
// 將來源記錄的表格資料帶入當前記錄
record.當前表格.value = res.record.來源表格.value
return event
})
這時就會遇到以下錯誤訊息:
Uncaught Error: app.record.edit.change.來源記錄號碼 is not allowed to return "Thenable" object.
這是因為 change 事件的事件處理器不支援 async 函式或回傳 Promise(即 Thenable 物件)。也就是說,不能在 change 事件中直接使用 async/await
或回傳一個包含非同步操作的處理流程。
既然 change 事件不允許回傳 Promise,那該如何實作需要非同步取得資料的需求呢?以下提供兩種常見作法:
setTimeout()
包裹非同步邏輯可以將非同步處理的部分放進 setTimeout
中,讓事件處理器本身仍保持同步,避免觸發錯誤。
kintone.events.on('app.record.edit.change.來源記錄號碼', event => {
const recordId = event.record.來源記錄號碼.value
setTimeout(async () => {
if (!recordId) return
try {
const res = await kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {
app: 來源應用程式ID,
id: recordId,
})
const { record } = kintone.app.record.get()
record.當前表格.value = res.record.來源表格.value
kintone.app.record.set({ record })
} catch (error) {
console.error('取得來源資料失敗', error)
}
}, 0)
return event
})
✅ 優點:
async/await
⚠️ 注意:
kintone.app.record.set()
更新畫面資料.then()
略晚.then()
處理流程另一種方式是不使用 async/await
,改用 .then()
語法,讓事件處理器仍為同步函式。callback 會被排入 microtask 隊列,在同步程式碼結束後立即執行。
kintone.events.on('app.record.edit.change.來源記錄號碼', event => {
const recordId = event.record.來源記錄號碼.value
if (!recordId) return event
kintone.api(kintone.api.url('/k/v1/record', true), 'GET', {
app: 來源應用程式ID,
id: recordId,
}).then(res => {
const { record } = kintone.app.record.get()
record.當前表格.value = res.record.來源表格.value
kintone.app.record.set({ record })
}).catch(error => {
console.error('取得來源資料失敗', error)
})
return event
})
✅ 優點:
setTimeout
更早⚠️ 注意:
kintone.app.record.set()
更新畫面資料.then()
的錯誤需用 .catch()
明確捕捉async/await
稍微不直覺。當邏輯較多、需串接多個 API 時,寫法容易變得巢狀、難以閱讀與維護不論是使用 setTimeout
或 .then()
,這類處理邏輯其實都已跳脫原本的 change 事件處理器本體,因此無法再透過 event.record
直接修改欄位資料。若要更新畫面上的欄位內容,需改用 kintone.app.record.set()
,並建議搭配 kintone.app.record.get()
先取得最新畫面資料後再進行修改,以減少資料被其他事件處理器覆蓋的風險。
實務上,無論是使用 setTimeout
或 .then()
,都存在一定的風險。這些方式本質上都是「延遲處理」,一旦應用中有較多客製化邏輯、註冊了多個事件處理器,就很難保證延後執行的程式不會與其他處理器產生衝突。例如可能出現彼此覆蓋欄位值、寫入時機錯亂等問題,特別是在多段程式都操作同一個欄位時更容易發生。
因此,雖然這些解法可以避開 change 事件不支援非同步的限制,但仍需謹慎評估使用時機。若流程較複雜或與其他邏輯高度耦合,建議重新思考整體設計,是否能將資料處理延後至 submit 階段,或集中於單一事件處理器中執行,以減少潛在衝突,提升可維護性。